/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2014-2021.
 * Description: main file of swift_irqsoff feature
 * Author: nixiaoming
 * Create: 2014-6-16
 */

#include <linux/kernel.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/kallsyms.h>
#include <linux/uaccess.h>
#include <linux/jiffies.h>
#include <linux/time.h>
#include <linux/sched.h>
#include <linux/sched/clock.h>
#include <linux/ftrace.h>
#include <linux/stacktrace.h>
#include <linux/types.h>
#include <linux/set_kbox_region.h>
#include <linux/rtos_sec.h>

#define SWIFT_IRQSOFF_INFO_FMT  "comm          : %s\n"\
				"pid           : %u\n"\
				"start time    : %llu\n"\
				"duration      : %llu\n"\
				"preempt count : %d\n"

#define SWIFT_IRQSOFF_INFO_BEGIN "<irq-state %s cpu-%d>\n"
#define SWIFT_IRQSOFF_INFO_END   "</irq-state>\n\n"

#define SIRQSOFF_LEVEL	CONFIG_RTOS_SWIFT_IRQSOFF_LEVEL
#if SIRQSOFF_LEVEL > 20
#error "RTOS_SWIFT_IRQSOFF_LEVEL configured value must be no greater than 20"
#endif

typedef struct {
	int working;				/* for nesting */
	int p_count;				/* preempt_count() */
	pid_t pid;				/* The pid of 'current' when irq was disable. */
	char comm[TASK_COMM_LEN];		/* comm of 'current' */
	u64 start_time;				/* start time of disable irq */
	u64 duration;				/* duration of irq-disable */
	unsigned long address[SIRQSOFF_LEVEL];  /* backtrace of 'current' */
} irq_state;

enum {
	SWIFT_IRQ_ON = 0,
	SWIFT_IRQ_OFF = 1,
};

enum {
	DURATION_CUR = 0,
	DURATION_MAX,
	DURATION_NOTE_NUM,
};

static DEFINE_PER_CPU(irq_state[DURATION_NOTE_NUM], irq_duration);

/* A flag to mark whether swift_irqsoff was enabled or not. */
static unsigned int swift_irqsoff_enable;
static unsigned int swift_irqsoff_enable_cmdline;

DEFINE_MUTEX(swift_lock);

static void reset_irq_state(void)
{
	int cpu;
	irq_state *cur = NULL;

	synchronize_rcu();
	for_each_online_cpu(cpu) {
		cur = per_cpu(irq_duration, cpu);
		memset(cur, 0, DURATION_NOTE_NUM * sizeof(irq_state));
	}
}

static void reset_irq_state_cur(void)
{
	int cpu;
	irq_state *cur = NULL;

	synchronize_rcu();
	for_each_online_cpu(cpu) {
		cur = per_cpu(irq_duration, cpu);
		memset(&cur[DURATION_CUR], 0, sizeof(irq_state));
	}
}

static void reset_irq_state_duration(void)
{
	int cpu;
	irq_state *cur = NULL;

	synchronize_rcu();
	for_each_online_cpu(cpu) {
		cur = per_cpu(irq_duration, cpu);
		if (cur[DURATION_CUR].start_time == 0)
			continue;

		cur[DURATION_CUR].duration = 0;
	}
}

static void enable_swift_irqsoff(void)
{
	if (swift_irqsoff_enable == 1)
		return;

	reset_irq_state_cur();
	swift_irqsoff_enable = 1;
}

static void disable_swift_irqsoff(void)
{
	if (swift_irqsoff_enable == 0)
		return;

	reset_irq_state_duration();
	swift_irqsoff_enable = 0;
}

/* save stack address */
static __always_inline void get_address(unsigned long *address, unsigned int size)
{
#ifdef CONFIG_ARM64
	stack_trace_save(address, size, 0);
#else
	struct stack_trace trace;

	trace.nr_entries	= 0;
	trace.max_entries	= size;
	trace.skip		= 0;
	trace.entries		= address;

	save_stack_trace(&trace);
#endif
}

/* trace irq_disable,  save stack , time */
void notrace swift_irqsoff_hardirqs_off(void)
{
	irq_state *state_array = NULL;
	irq_state *state_cur = NULL;
	int tmp_pcount;

	if (likely(swift_irqsoff_enable == 0))
		return;
	tmp_pcount = preempt_count();
	/*
	 * get_cpu_var call preempt_disable, cause preempt_count change,
	 * so save preempt_count first
	 */
	state_array = get_cpu_var(irq_duration);
	state_cur = state_array + DURATION_CUR;

	if (unlikely(state_cur->working == SWIFT_IRQ_OFF)) {
		/* Irq had been disabled! */
		put_cpu_var(irq_duration);
		return;
	}

	/*
	 * We get the timestamp first, because the instructions
	 * below may cost a lot of time.
	 */
	state_cur->start_time	= sched_clock();
	state_cur->working	= SWIFT_IRQ_OFF;
	state_cur->duration	= 0; /* mark irq disable */
	state_cur->pid		= current->pid;
	state_cur->p_count	= tmp_pcount;
	strlcpy(state_cur->comm, current->comm, sizeof(state_cur->comm));
	get_address(state_cur->address, SIRQSOFF_LEVEL);

	put_cpu_var(irq_duration);
}
EXPORT_SYMBOL(swift_irqsoff_hardirqs_off);


/* trace irq_enable, count irq disable duration time */
void notrace swift_irqsoff_hardirqs_on(void)
{
	irq_state *state_array = NULL;
	irq_state *state_cur = NULL;
	irq_state *state_max = NULL;

	if (likely(swift_irqsoff_enable == 0))
		return;

	state_array = get_cpu_var(irq_duration);

	state_cur = state_array + DURATION_CUR;
	state_max = state_array + DURATION_MAX;

	if (unlikely(state_cur->working == SWIFT_IRQ_ON)) {
		/* Irq had been enabled! */
		put_cpu_var(irq_duration);
		return;
	}

	/*
	 * To avoid a situation, in which current-duration equal to sched_clock() :
	 * ...
	 * -> irq disable
	 * ...
	 * -> disable swift_irqsoff
	 * ...
	 * -> enable swift_irqsoff : reset cur->start_time to 0
	 * ...
	 * -> irq enable : calc duration, sched_clock - cur->start_time
	 *
	 * -> here, we get swift_irqsoff info, but current-duration equal to sched_clock
	 * ...
	 */
	/* Illegal start_time */
	if (unlikely(state_cur->start_time == 0)) {
		put_cpu_var(irq_duration);
		return;
	}

	state_cur->working = SWIFT_IRQ_ON;
	state_cur->duration = sched_clock() - state_cur->start_time;

	if (unlikely(state_max->duration < state_cur->duration))
		*state_max = *state_cur;

	state_cur->start_time = 0;
	state_cur->duration = 0;

	put_cpu_var(irq_duration);
}
EXPORT_SYMBOL(swift_irqsoff_hardirqs_on);

/*
 * KVM nVHE code in arch/arm64/include/asm/hyp_image.h
 * uses macro kvm_nvhe_sym to prefixed with __kvm_nvhe_,
 * to separate it from the kernel proper.
 */
void notrace __kvm_nvhe_swift_irqsoff_hardirqs_on(void)
{
	swift_irqsoff_hardirqs_on();
}
EXPORT_SYMBOL(__kvm_nvhe_swift_irqsoff_hardirqs_on);

void notrace __kvm_nvhe_swift_irqsoff_hardirqs_off(void)
{
	swift_irqsoff_hardirqs_off();
}
EXPORT_SYMBOL(__kvm_nvhe_swift_irqsoff_hardirqs_off);

#define DEFAULT_BUFFER_SIZE 256

static void print_info(struct seq_file *p, const char *fmt, ...)
{
	char buffer[DEFAULT_BUFFER_SIZE] = {0};
	va_list args;
	int ret = 0;
	int region;

	va_start(args, fmt);
	ret = vscnprintf(buffer, DEFAULT_BUFFER_SIZE, fmt, args);
	va_end(args);

	if (ret == 0) {
		pr_err("%s : DEFAULT_BUFFER_SIZE is not enough to hold data.\n", __func__);
		return;
	}

	if (p != NULL) {
		seq_printf(p, "%s", buffer);
	} else {
		region = get_kbox_region();
		kern_kbox_write(region, buffer, min(ret, DEFAULT_BUFFER_SIZE));
	}
}

/*
 * print the irq_state
 * if p is not NULL, we print to seq_file,
 * otherwise we print by printk.
 */
static int print_irq_state(struct seq_file *p, const irq_state *state,
			   const int cpu_no, const char *flag)
{
	int i = 0;

	if (state == NULL)
		return -EINVAL;

	print_info(p, SWIFT_IRQSOFF_INFO_BEGIN, flag, cpu_no);

	if (swift_irqsoff_enable == 0) {
		print_info(p, "swift_irqsoff is disabled.\n");
	} else if (state->start_time == 0) {
		print_info(p, "irq is enabled.\n");
	} else {
		print_info(p, SWIFT_IRQSOFF_INFO_FMT, state->comm, state->pid,
			   state->start_time, state->duration, state->p_count);

		for (i = 0; i < SIRQSOFF_LEVEL; i++) {
			const unsigned long address = state->address[i];

			if (address == ULONG_MAX)
				break;

			print_info(p, "%pS\n", (void *)address);
		}
	}

	print_info(p, SWIFT_IRQSOFF_INFO_END);

	return 0;
}

/* api of display info */
void  display_swift_irqsoff(void)
{
	int cpu_no;
	int region;
	irq_state *state_cur = NULL;
	irq_state *state_max = NULL;
	irq_state *state_array = NULL;
	const char *str_log = "swift_irqsoff is disabled\n";

	if (swift_irqsoff_enable == 0) {
		region = get_kbox_region();
		kern_kbox_write(region, str_log, strlen(str_log));
		return;
	}

	state_array = get_cpu_var(irq_duration);

	/* get_cpu_var did preemp_disable already. */
	cpu_no = smp_processor_id();

	/* current */
	state_cur = state_array + DURATION_CUR;
	(void)print_irq_state(NULL, state_cur, cpu_no, "current");

	/* max */
	state_max = state_array + DURATION_MAX;
	(void)print_irq_state(NULL, state_max, cpu_no, "max");

	put_cpu_var(irq_duration);
}
EXPORT_SYMBOL(display_swift_irqsoff);

/* display swift_irqsoff info */
static int  proc_show_seq_show(struct seq_file *p, void *v)
{
	loff_t cpu_no = *(loff_t *)v;
	irq_state *state_array = NULL;
	irq_state *state_cur = NULL;
	irq_state *state_max = NULL;
	int ret = 0;

	if (cpu_no >= num_online_cpus())
		return 0;

	state_array = per_cpu(irq_duration, cpu_no);

	/* current */
	state_cur = state_array + DURATION_CUR;
	ret = print_irq_state(p, state_cur, cpu_no, "current");
	if (ret < 0)
		return ret;

	/* max */
	state_max = state_array + DURATION_MAX;
	ret = print_irq_state(p, state_max, cpu_no, "max");
	return ret;
}

static void *proc_show_seq_start(struct seq_file *s, loff_t *pos)
{
	return (*pos < num_online_cpus()) ? pos : NULL;
}

static void *proc_show_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
	(*pos)++;
	return (*pos < num_online_cpus()) ? pos : NULL;
}

static void proc_show_seq_stop(struct seq_file *s, void *v)
{
}

static const struct seq_operations proc_show_seq_ops = {
	.start = proc_show_seq_start,
	.next  = proc_show_seq_next,
	.stop  = proc_show_seq_stop,
	.show  = proc_show_seq_show,
};

static int proc_show_open(struct inode *inode, struct file *filp)
{
	if (check_msg_restrict()) {
		pr_err("swift_irqsoff: check permissions fail on open\n");
		return -EPERM;
	}
	return seq_open(filp, &proc_show_seq_ops);
}

static ssize_t proc_show_write(struct file *file, const char __user *buf,
			       size_t count, loff_t *offs)
{
	if (count) {
		char c = 0;

		if (get_user(c, buf))
			return -EFAULT;

		/* To echo 0 > show, we clear irq-state info. */
		if (c == '0' && swift_irqsoff_enable == 0) {
			mutex_lock(&swift_lock);
			reset_irq_state();
			mutex_unlock(&swift_lock);
		} else {
			return -EINVAL;
		}
	}

	return  count;
}

static const struct proc_ops proc_show_fops = {
	.proc_open    = proc_show_open,
	.proc_read    = seq_read,
	.proc_write   = proc_show_write,
	.proc_lseek   = seq_lseek,
	.proc_release = seq_release,
};

static ssize_t proc_enable_read(struct file *file, char __user *buf,
				size_t count, loff_t *ppos)
{
	char buffer[5];

	snprintf(buffer, sizeof(buffer), "%d\n", swift_irqsoff_enable);
	return simple_read_from_buffer(buf, count, ppos, buffer, strlen(buffer));
}


static ssize_t proc_enable_write(struct file *file, const char __user *buf,
				 size_t count, loff_t *offs)
{
	if (count) {
		char c;

		if (get_user(c, buf))
			return -EFAULT;

		switch (c) {
		case '0':
			mutex_lock(&swift_lock);
			disable_swift_irqsoff();
			mutex_unlock(&swift_lock);
			break;
		case '1':
			mutex_lock(&swift_lock);
			enable_swift_irqsoff();
			mutex_unlock(&swift_lock);
			break;
		default:
			return -EINVAL;
		}
	}

	return  count;
}

static const struct proc_ops proc_enable_fops = {
	.proc_read   = proc_enable_read,
	.proc_write  = proc_enable_write,
};

static int __init init_swift_irqsoff_proc(void)
{
	struct proc_dir_entry *root_swift_irqsoff_dir = NULL;
	struct proc_dir_entry *pe = NULL;

	if (!swift_irqsoff_enable_cmdline)
		return -EINVAL;

	root_swift_irqsoff_dir = proc_mkdir("swift_irqsoff", NULL);
	if (!root_swift_irqsoff_dir)
		return -ENOMEM;

	pe = proc_create("show", 0640, root_swift_irqsoff_dir, &proc_show_fops);
	if (!pe)
		goto out_show;

	pe = proc_create("enable", 0640, root_swift_irqsoff_dir, &proc_enable_fops);
	if (!pe)
		goto out_enable;

	reset_irq_state();

	return 0;

out_enable:
	remove_proc_entry("show", root_swift_irqsoff_dir);
out_show:
	remove_proc_entry("swift_irqsoff", NULL);
	return -ENOMEM;
}

module_init(init_swift_irqsoff_proc);

static int __init setup_swift_irqsoff_enable(char *str)
{
	swift_irqsoff_enable_cmdline = 1;
	return 0;
}
__setup("swift_irqsoff_enable", setup_swift_irqsoff_enable);
